/**
 * Copyright (c) 2006-2020, JGraph Ltd
 * Copyright (c) 2006-2020, draw.io AG
 */

//Add a closure to hide the class private variables without changing the code a lot
(function () {

    var _token = null;

    window.OneDriveClient = function (editorUi, isExtAuth, inlinePicker, noLogout) {
        if (isExtAuth == null && window.urlParams != null && window.urlParams['extAuth'] == '1') {
            isExtAuth = true;
        }

        if (inlinePicker == null &&
            ((window.urlParams != null && window.urlParams['inlinePicker'] == '1') ||
                mxClient.IS_ANDROID || mxClient.IS_IOS //Mobile devices doesn't work with OneDrive picker, so, use the inline picker
            )) {
            inlinePicker = true;
        }

        if (noLogout == null && window.urlParams != null && window.urlParams['noLogoutOD'] == '1') {
            noLogout = true;
        }

        DrawioClient.call(this, editorUi, isExtAuth ? 'oneDriveExtAuthInfo' : 'oneDriveAuthInfo');

        this.isExtAuth = isExtAuth;
        this.inlinePicker = inlinePicker;
        this.noLogout = noLogout;
        var authInfo = JSON.parse(this.token);

        if (authInfo != null) {
            this.endpointHint = authInfo.endpointHint != null ? authInfo.endpointHint.replace('/Documents', '/_layouts/15/onedrive.aspx') : authInfo.endpointHint;
        }
    };

// Extends DrawioClient
    mxUtils.extend(OneDriveClient, DrawioClient);

    /**
     * Specifies if thumbnails should be enabled. Default is true.
     * LATER: If thumbnails are disabled, make sure to replace the
     * existing thumbnail with the placeholder only once.
     */
    OneDriveClient.prototype.clientId = window.DRAWIO_MSGRAPH_CLIENT_ID || ((window.location.hostname == 'test.draw.io') ?
        '2e598409-107f-4b59-89ca-d7723c8e00a4' : '45c10911-200f-4e27-a666-9e9fca147395');

    OneDriveClient.prototype.clientId = window.location.hostname == 'app.diagrams.net' ?
        'b5ff67d6-3155-4fca-965a-59a3655c4476' : OneDriveClient.prototype.clientId;

    OneDriveClient.prototype.clientId = window.location.hostname == 'viewer.diagrams.net' ?
        '417a451a-a343-4788-b6c1-901e63182565' : OneDriveClient.prototype.clientId;
    /**
     * OAuth 2.0 scopes for installing Drive Apps.
     */
    OneDriveClient.prototype.scopes = 'user.read files.readwrite.all sites.read.all';

    /**
     * OAuth 2.0 scopes for installing Drive Apps.
     */
    OneDriveClient.prototype.redirectUri = window.location.protocol + '//' + window.location.host + '/microsoft';
    OneDriveClient.prototype.pickerRedirectUri = window.location.protocol + '//' + window.location.host + '/onedrive3.html';

    /**
     * This is the default endpoint for personal accounts
     */
    OneDriveClient.prototype.defEndpointHint = 'api.onedrive.com';
    OneDriveClient.prototype.endpointHint = OneDriveClient.prototype.defEndpointHint;

    /**
     * Executes the first step for connecting to Google Drive.
     */
    OneDriveClient.prototype.extension = '.drawio';

    /**
     * Executes the first step for connecting to Google Drive.
     */
    OneDriveClient.prototype.baseUrl = 'https://graph.microsoft.com/v1.0';

    /**
     * Empty function used when no callback is needed
     */
    OneDriveClient.prototype.emptyFn = function () {
    };

    OneDriveClient.prototype.invalidFilenameRegExs = [
        /[~"#%\*:<>\?\/\\{\|}]/,
        /^\.lock$/i,
        /^CON$/i,
        /^PRN$/i,
        /^AUX$/i,
        /^NUL$/i,
        /^COM\d$/i,
        /^LPT\d$/i,
        /^desktop\.ini$/i,
        /_vti_/i
    ];

    /**
     * Check if the file/folder name is valid
     */
    OneDriveClient.prototype.isValidFilename = function (filename) {
        if (filename == null || filename === '') return false;

        for (var i = 0; i < this.invalidFilenameRegExs.length; i++) {
            if (this.invalidFilenameRegExs[i].test(filename)) return false;
        }

        return true;
    };


    /**
     * Checks if the client is authorized and calls the next step.
     */
    OneDriveClient.prototype.get = function (url, onload, onerror) {
        var req = new mxXmlRequest(url, null, 'GET');

        req.setRequestHeaders = mxUtils.bind(this, function (request, params) {
            request.setRequestHeader('Authorization', 'Bearer ' + _token);
        });

        req.send(onload, onerror);

        return req;
    };

    /**
     * Checks if the client is authorized and calls the next step.
     */
    OneDriveClient.prototype.updateUser = function (success, error, failOnAuth) {
        var acceptResponse = true;

        var timeoutThread = window.setTimeout(mxUtils.bind(this, function () {
            acceptResponse = false;
            error({code: App.ERROR_TIMEOUT});
        }), this.ui.timeout);

        this.get(this.baseUrl + '/me', mxUtils.bind(this, function (req) {
            window.clearTimeout(timeoutThread);

            if (acceptResponse) {
                if (req.getStatus() < 200 || req.getStatus() >= 300) {
                    if (!failOnAuth) {
                        this.logout();

                        this.authenticate(mxUtils.bind(this, function () {
                            this.updateUser(success, error, true);
                        }), error);
                    } else {
                        error({message: mxResources.get('accessDenied')});
                    }
                } else {
                    var data = JSON.parse(req.getText());
                    this.setUser(new DrawioUser(data.id, null, data.displayName));
                    success();
                }
            }
        }), mxUtils.bind(this, function (err) {
            window.clearTimeout(timeoutThread);

            if (acceptResponse) {
                error(err);
            }
        }));
    };

    OneDriveClient.prototype.resetTokenRefresh = function (expires_in) {
        if (this.tokenRefreshThread != null) {
            window.clearTimeout(this.tokenRefreshThread);
            this.tokenRefreshThread = null;
        }

        // Starts timer to refresh token before it expires
        if (expires_in > 0) {
            this.tokenRefreshInterval = expires_in * 1000;

            this.tokenRefreshThread = window.setTimeout(mxUtils.bind(this, function () {
                //Get a new fresh accessToken
                this.authenticate(this.emptyFn, this.emptyFn, true);
            }), expires_in * 900);
        }
    };


    /**
     * Authorizes the client, gets the userId and calls <open>.
     */
    OneDriveClient.prototype.authenticate = function (success, error, failOnAuth) {
        if (this.isExtAuth) {
            window.parent.oneDriveAuth(mxUtils.bind(this, function (newAuthInfo) {
                this.updateAuthInfo(newAuthInfo, true, this.endpointHint == null, success, error);
            }), error, window.urlParams != null && urlParams['odAuthCancellable'] == '1');
            return;
        }

        var req = new mxXmlRequest(this.redirectUri + '?getState=1', null, 'GET');

        req.send(mxUtils.bind(this, function (req) {
            if (req.getStatus() >= 200 && req.getStatus() <= 299) {
                this.authenticateStep2(req.getText(), success, error, failOnAuth);
            } else if (error != null) {
                error(req);
            }
        }), error);
    };

    OneDriveClient.prototype.updateAuthInfo = function (newAuthInfo, remember, forceUserUpdate, success, error) {
        if (forceUserUpdate) {
            this.setUser(null);
        }

        _token = newAuthInfo.access_token;
        delete newAuthInfo.access_token; //Don't store access token
        newAuthInfo.expiresOn = Date.now() + newAuthInfo.expires_in * 1000;
        this.tokenExpiresOn = newAuthInfo.expiresOn;

        newAuthInfo.remember = remember;
        this.setPersistentToken(JSON.stringify(newAuthInfo), !remember);
        this.resetTokenRefresh(newAuthInfo.expires_in);

        if (forceUserUpdate) {
            //Find out the type of the account + endpoint
            this.getAccountTypeAndEndpoint(mxUtils.bind(this, function () {
                success();
            }), error);
        } else {
            success();
        }
    };

    OneDriveClient.prototype.authenticateStep2 = function (state, success, error, failOnAuth) {
        if (window.onOneDriveCallback == null) {
            var auth = mxUtils.bind(this, function () {
                var acceptAuthResponse = true;

                //Retry request with refreshed token
                var authInfo = JSON.parse(this.getPersistentToken(true));

                if (authInfo != null) {
                    var req = new mxXmlRequest(this.redirectUri + '?state=' + encodeURIComponent('cId=' + this.clientId + '&domain=' + window.location.hostname + '&token=' + state), null, 'GET'); //To identify which app/domain is used

                    req.send(mxUtils.bind(this, function (req) {
                        if (req.getStatus() >= 200 && req.getStatus() <= 299) {
                            this.updateAuthInfo(JSON.parse(req.getText()), authInfo.remember, false, success, error);
                        } else {
                            this.clearPersistentToken();
                            this.setUser(null);
                            _token = null;

                            if (req.getStatus() == 401 && !failOnAuth) // (Unauthorized) [e.g, invalid refresh token]
                            {
                                auth();
                            } else {
                                error({message: mxResources.get('accessDenied'), retry: auth});
                            }
                        }
                    }), error);
                } else {
                    this.ui.showAuthDialog(this, true, mxUtils.bind(this, function (remember, authSuccess) {
                        var url = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize' +
                            '?client_id=' + this.clientId + '&response_type=code' +
                            '&redirect_uri=' + encodeURIComponent(this.redirectUri) +
                            '&scope=' + encodeURIComponent(this.scopes + (remember ? ' offline_access' : '')) +
                            '&state=' + encodeURIComponent('cId=' + this.clientId + '&domain=' + window.location.hostname + '&token=' + state); //To identify which app/domain is used

                        var width = 525,
                            height = 525,
                            screenX = window.screenX,
                            screenY = window.screenY,
                            outerWidth = window.outerWidth,
                            outerHeight = window.outerHeight;

                        var left = screenX + Math.max(outerWidth - width, 0) / 2;
                        var top = screenY + Math.max(outerHeight - height, 0) / 2;

                        var features = ['width=' + width, 'height=' + height,
                            'top=' + top, 'left=' + left,
                            'status=no', 'resizable=yes',
                            'toolbar=no', 'menubar=no',
                            'scrollbars=yes'];
                        var popup = window.open(url, 'odauth', features.join(','));

                        if (popup != null) {
                            window.onOneDriveCallback = mxUtils.bind(this, function (authInfo, authWindow) {
                                if (acceptAuthResponse) {
                                    window.onOneDriveCallback = null;
                                    acceptAuthResponse = false;

                                    try {
                                        if (authInfo == null) {
                                            error({message: mxResources.get('accessDenied'), retry: auth});
                                        } else {
                                            if (authSuccess != null) {
                                                authSuccess();
                                            }

                                            this.updateAuthInfo(authInfo, remember, true, success, error);
                                        }
                                    } catch (e) {
                                        error(e);
                                    } finally {
                                        if (authWindow != null) {
                                            authWindow.close();
                                        }
                                    }
                                } else if (authWindow != null) {
                                    authWindow.close();
                                }
                            });

                            popup.focus();
                        }
                    }), mxUtils.bind(this, function () {
                        if (acceptAuthResponse) {
                            window.onOneDriveCallback = null;
                            acceptAuthResponse = false;
                            error({message: mxResources.get('accessDenied'), retry: auth});
                        }
                    }));
                }
            });

            auth();
        } else {
            error({code: App.ERROR_BUSY});
        }
    };


    OneDriveClient.prototype.getAccountTypeAndEndpoint = function (success, error) {
        this.get(this.baseUrl + '/me/drive/root', mxUtils.bind(this, function (req) {
            try {
                if (req.getStatus() >= 200 && req.getStatus() <= 299) {
                    var resp = JSON.parse(req.getText());

                    if (resp.webUrl.indexOf('.sharepoint.com') > 0) {
                        //TODO Confirm this works with all sharepoint sites
                        this.endpointHint = resp.webUrl.replace('/Documents', '/_layouts/15/onedrive.aspx');
                    } else {
                        this.endpointHint = this.defEndpointHint;
                    }

                    //Update authInfo with endpointHint
                    var authInfo = JSON.parse(this.getPersistentToken(true));

                    if (authInfo != null) {
                        authInfo.endpointHint = this.endpointHint;
                        this.setPersistentToken(JSON.stringify(authInfo), !authInfo.remember);
                    }

                    success();
                    return;
                }
            } catch (e) {
            }
            //It is expected to work as this call immediately follows getting a fresh access token
            error({message: mxResources.get('unknownError') + ' (Code: ' + req.getStatus() + ')'});

        }), error);
    };

    /**
     * Checks if the client is authorized and calls the next step.
     */
    OneDriveClient.prototype.executeRequest = function (url, success, error) {
        var doExecute = mxUtils.bind(this, function (failOnAuth) {
            var acceptResponse = true;

            var timeoutThread = window.setTimeout(mxUtils.bind(this, function () {
                acceptResponse = false;
                error({code: App.ERROR_TIMEOUT, retry: doExecute});
            }), this.ui.timeout);

            this.get(url, mxUtils.bind(this, function (req) {
                window.clearTimeout(timeoutThread);

                if (acceptResponse) {
                    // 404 (file not found) is a valid response for checkExists
                    if ((req.getStatus() >= 200 && req.getStatus() <= 299) || req.getStatus() == 404) {
                        if (this.user == null) {
                            this.updateUser(this.emptyFn, this.emptyFn, true);
                        }

                        success(req);
                    }
                    // 400 is returns if wrong user for this file
                    else if (!failOnAuth && (req.getStatus() === 401 || req.getStatus() === 400)) {
                        //Authorize again using the refresh token
                        this.authenticate(function () {
                            doExecute(true);
                        }, error, failOnAuth);
                    } else {
                        error(this.parseRequestText(req));
                    }
                }
            }), mxUtils.bind(this, function (err) {
                window.clearTimeout(timeoutThread);

                if (acceptResponse) {
                    error(err);
                }
            }));
        });

        if (_token == null || this.tokenExpiresOn - Date.now() < 60000) //60 sec tolerance window
        {
            this.authenticate(function () {
                doExecute(true);
            }, error);
        } else {
            doExecute(false);
        }
    };

    /**
     * Checks if the client is authorized and calls the next step.
     */
    OneDriveClient.prototype.checkToken = function (fn) {
        if (_token == null || this.tokenRefreshThread == null || this.tokenExpiresOn - Date.now() < 60000) {
            this.authenticate(fn, this.emptyFn);
        } else {
            fn();
        }
    };

    OneDriveClient.prototype.getItemRef = function (id) {
        var idParts = id.split('/');

        if (idParts.length > 1) {
            return {driveId: idParts[0], id: idParts[1]};
        } else {
            return {id: id};
        }
    };

    OneDriveClient.prototype.getItemURL = function (id, relative) {
        var idParts = id.split('/');

        if (idParts.length > 1) {
            var driveId = idParts[0];
            var itemId = idParts[1];
            return (relative ? '' : this.baseUrl) + '/drives/' + driveId + (itemId == 'root' ? '/root' : '/items/' + itemId);
        } else {
            return (relative ? '' : this.baseUrl) + '/me/drive/items/' + id;
        }
    };

    /**
     * Checks if the client is authorized and calls the next step.
     */
    OneDriveClient.prototype.getLibrary = function (id, success, error) {
        this.getFile(id, success, error, false, true);
    };

    /**
     * Workaround for added content to HTML files in Sharepoint.
     */
    OneDriveClient.prototype.removeExtraHtmlContent = function (data) {
        var idx = data.lastIndexOf('<html><head><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8"><meta name="Robots" ');

        if (idx > 0) {
            data = data.substring(0, idx);
        }

        return data;
    };

    /**
     * Checks if the client is authorized and calls the next step.
     */
    OneDriveClient.prototype.getFile = function (id, success, error, denyConvert, asLibrary) {
        asLibrary = (asLibrary != null) ? asLibrary : false;

        this.executeRequest(this.getItemURL(id), mxUtils.bind(this, function (req) {
            if (req.getStatus() >= 200 && req.getStatus() <= 299) {
                var meta = JSON.parse(req.getText());
                var binary = /\.png$/i.test(meta.name);

                // Handles .vsdx, Gliffy and PNG+XML files by creating a temporary file
                if (/\.v(dx|sdx?)$/i.test(meta.name) || /\.gliffy$/i.test(meta.name) ||
                    /\.pdf$/i.test(meta.name) || (!this.ui.useCanvasForExport && binary)) {
                    var mimeType = (meta.file != null) ? meta.file.mimeType : null;
                    this.ui.convertFile(meta['@microsoft.graph.downloadUrl'], meta.name, mimeType,
                        this.extension, success, error);
                } else {
                    var acceptResponse = true;

                    var timeoutThread = window.setTimeout(mxUtils.bind(this, function () {
                        acceptResponse = false;
                        error({code: App.ERROR_TIMEOUT})
                    }), this.ui.timeout);

                    this.ui.editor.loadUrl(meta['@microsoft.graph.downloadUrl'], mxUtils.bind(this, function (data) {
                        try {
                            window.clearTimeout(timeoutThread);

                            if (acceptResponse) {
                                if (/\.html$/i.test(meta.name)) {
                                    data = this.removeExtraHtmlContent(data);
                                }

                                var index = (binary) ? data.lastIndexOf(',') : -1;
                                var file = null;

                                if (index > 0) {
                                    var xml = this.ui.extractGraphModelFromPng(data);

                                    if (xml != null && xml.length > 0) {
                                        data = xml;
                                    } else {
                                        // Imports as PNG image
                                        file = new LocalFile(this.ui, data, meta.name, true);
                                    }
                                }
                                // Checks for base64 encoded mxfile
                                else if (data.substring(0, 32) == '') {
                                    var temp = data.substring(22);
                                    data = (window.atob && !mxClient.IS_SF) ? atob(temp) : Base64.decode(temp);
                                }

                                if (Graph.fileSupport && new XMLHttpRequest().upload && this.ui.isRemoteFileFormat(data, meta['@microsoft.graph.downloadUrl'])) {
                                    this.ui.parseFile(new Blob([data], {type: 'application/octet-stream'}), mxUtils.bind(this, function (xhr) {
                                        try {
                                            if (xhr.readyState == 4) {
                                                if (xhr.status >= 200 && xhr.status <= 299) {
                                                    success(new LocalFile(this.ui, xhr.responseText, meta.name + this.extension, true));
                                                } else if (error != null) {
                                                    error({message: mxResources.get('errorLoadingFile')});
                                                }
                                            }
                                        } catch (e) {
                                            if (error != null) {
                                                error(e);
                                            } else {
                                                throw e;
                                            }
                                        }
                                    }), meta.name);
                                } else {
                                    if (file != null) {
                                        success(file);
                                    } else if (asLibrary) {
                                        success(new OneDriveLibrary(this.ui, data, meta));
                                    } else {
                                        success(new OneDriveFile(this.ui, data, meta));
                                    }
                                }
                            }
                        } catch (e) {
                            if (error != null) {
                                error(e);
                            } else {
                                throw e;
                            }
                        }
                    }), mxUtils.bind(this, function (req) {
                        window.clearTimeout(timeoutThread);

                        if (acceptResponse) {
                            error(this.parseRequestText(req));
                        }
                    }), binary || (meta.file != null && meta.file.mimeType != null &&
                        (meta.file.mimeType.substring(0, 6) == 'image/' ||
                            meta.file.mimeType == 'application/pdf')));
                }
            } else {
                error(this.parseRequestText(req));
            }
        }), error);
    };

    /**
     * Translates this point by the given vector.
     *
     * @param {number} dx X-coordinate of the translation.
     * @param {number} dy Y-coordinate of the translation.
     */
    OneDriveClient.prototype.renameFile = function (file, filename, success, error) {
        if (file != null && filename != null) {
            if (!this.isValidFilename(filename)) {
                error({
                    message: this.invalidFilenameRegExs[0].test(filename) ?
                        mxResources.get('oneDriveCharsNotAllowed') : mxResources.get('oneDriveInvalidDeviceName')
                });
                return;
            }

            // TODO: How to force overwrite file with same name?
            this.checkExists(file.getParentId(), filename, false, mxUtils.bind(this, function (checked) {
                if (checked) {
                    this.writeFile(this.getItemURL(file.getId()), JSON.stringify({name: filename}), 'PATCH', 'application/json', success, error);
                } else {
                    error();
                }
            }));
        }
    };

    /**
     * Translates this point by the given vector.
     *
     * @param {number} dx X-coordinate of the translation.
     * @param {number} dy Y-coordinate of the translation.
     */
    OneDriveClient.prototype.moveFile = function (id, folderId, success, error) {
        //check that the source and destination are on the same drive
        var folderInfo = this.getItemRef(folderId);
        var fileInfo = this.getItemRef(id);

        if (folderInfo.driveId != fileInfo.driveId) {
            error({message: mxResources.get('cannotMoveOneDrive', null, 'Moving a file between accounts is not supported yet.')});
        } else {
            this.writeFile(this.getItemURL(id), JSON.stringify({parentReference: folderInfo}), 'PATCH', 'application/json', success, error);
        }
    };

    /**
     * Translates this point by the given vector.
     *
     * @param {number} dx X-coordinate of the translation.
     * @param {number} dy Y-coordinate of the translation.
     */
    OneDriveClient.prototype.insertLibrary = function (filename, data, success, error, folderId) {
        this.insertFile(filename, data, success, error, true, folderId);
    };

    /**
     * Translates this point by the given vector.
     *
     * @param {number} dx X-coordinate of the translation.
     * @param {number} dy Y-coordinate of the translation.
     */
    OneDriveClient.prototype.insertFile = function (filename, data, success, error, asLibrary, folderId) {
        if (!this.isValidFilename(filename)) {
            error({
                message: this.invalidFilenameRegExs[0].test(filename) ?
                    mxResources.get('oneDriveCharsNotAllowed') : mxResources.get('oneDriveInvalidDeviceName')
            });
            return;
        }

        asLibrary = (asLibrary != null) ? asLibrary : false;

        this.checkExists(folderId, filename, true, mxUtils.bind(this, function (checked) {
            if (checked) {
                var folder = '/me/drive/root';

                if (folderId != null) {
                    folder = this.getItemURL(folderId, true);
                }

                var insertSuccess = mxUtils.bind(this, function (meta) {
                    if (asLibrary) {
                        success(new OneDriveLibrary(this.ui, data, meta));
                    } else {
                        success(new OneDriveFile(this.ui, data, meta));
                    }
                });

                var url = this.baseUrl + folder + '/children/' + encodeURIComponent(filename) + '/content';

                //OneDrive has a limit on PUT API of 4MB, larger files needs to use the upload session method
                if (data.length >= 4000000 /*4MB*/) {
                    //Create empty file first then upload. TODO Can we get an upload session for non-existing files?
                    this.writeFile(url, '', 'PUT', null, mxUtils.bind(this, function (meta) {
                        this.writeLargeFile(this.getItemURL(meta.id), data, insertSuccess, error);
                    }), error);
                } else {
                    this.writeFile(url, data, 'PUT', null, insertSuccess, error);
                }
            } else {
                error();
            }
        }))
    };

    /**
     * Translates this point by the given vector.
     *
     * @param {number} dx X-coordinate of the translation.
     * @param {number} dy Y-coordinate of the translation.
     */
    OneDriveClient.prototype.checkExists = function (parentId, filename, askReplace, fn) {
        var folder = '/me/drive/root';

        if (parentId != null) {
            folder = this.getItemURL(parentId, true);
        }

        this.executeRequest(this.baseUrl + folder + '/children/' + encodeURIComponent(filename), mxUtils.bind(this, function (req) {
            if (req.getStatus() == 404) {
                fn(true);
            } else {
                if (askReplace) {
                    this.ui.spinner.stop();

                    this.ui.confirm(mxResources.get('replaceIt', [filename]), function () {
                        fn(true);
                    }, function () {
                        fn(false);
                    });
                } else {
                    this.ui.spinner.stop();

                    this.ui.showError(mxResources.get('error'), mxResources.get('fileExists'), mxResources.get('ok'), function () {
                        fn(false);
                    });
                }
            }
        }), function (req) {
            fn(false);
        }, true);
    };

    /**
     * Translates this point by the given vector.
     *
     * @param {number} dx X-coordinate of the translation.
     * @param {number} dy Y-coordinate of the translation.
     */
    OneDriveClient.prototype.saveFile = function (file, success, error, etag) {
        try {
            var savedData = file.getData();

            var fn = mxUtils.bind(this, function (data) {
                var saveSuccess = mxUtils.bind(this, function (resp) {
                    success(resp, savedData);
                });

                var url = this.getItemURL(file.getId());

                //OneDrive has a limit on PUT API of 4MB, larger files needs to use the upload session method
                if (data.length >= 4000000 /*4MB*/) {
                    this.writeLargeFile(url, data, saveSuccess, error, etag);
                } else {
                    this.writeFile(url + '/content/', data, 'PUT', null, saveSuccess, error, etag);
                }
            });

            if (this.ui.useCanvasForExport && /(\.png)$/i.test(file.meta.name)) {
                var p = this.ui.getPngFileProperties(this.ui.fileNode);

                this.ui.getEmbeddedPng(mxUtils.bind(this, function (data) {
                    fn(this.ui.base64ToBlob(data, 'image/png'));
                }), error, (this.ui.getCurrentFile() != file) ?
                    savedData : null, p.scale, p.border);
            } else {
                fn(savedData);
            }
        } catch (e) {
            error(e);
        }
    };

    OneDriveClient.prototype.writeLargeFile = function (url, data, success, error, etag) {
        try {
            var chunkSize = 4 * 1024 * 1024; //4MB chunk;

            if (data != null) {
                var uploadPart = mxUtils.bind(this, function (uploadUrl, index, retryCount) {
                    try {
                        retryCount = retryCount || 0;
                        var acceptResponse = true;
                        var timeoutThread = null;

                        timeoutThread = window.setTimeout(mxUtils.bind(this, function () {
                            acceptResponse = false;
                            error({code: App.ERROR_TIMEOUT});
                        }), this.ui.timeout);

                        var part = data.substr(index, chunkSize);
                        var req = new mxXmlRequest(uploadUrl, part, 'PUT');

                        req.setRequestHeaders = mxUtils.bind(this, function (request, params) {
                            request.setRequestHeader('Content-Length', part.length);
                            request.setRequestHeader('Content-Range', 'bytes ' + index + '-' + (index + part.length - 1) + '/' + data.length);
                        });

                        req.send(mxUtils.bind(this, function (req) {
                            window.clearTimeout(timeoutThread);

                            if (acceptResponse) {
                                var status = req.getStatus();
                                if (status >= 200 && status <= 299) {
                                    var nextByte = index + part.length;

                                    if (nextByte == data.length) {
                                        success(JSON.parse(req.getText()));
                                    } else {
                                        uploadPart(uploadUrl, nextByte, retryCount);
                                    }
                                } else if (status >= 500 && status <= 599 && retryCount < 2) //Retry on server errors
                                {
                                    retryCount++;
                                    uploadPart(uploadUrl, index, retryCount);
                                } else {
                                    error(this.parseRequestText(req), req);
                                }
                            }
                        }), mxUtils.bind(this, function (req) {
                            window.clearTimeout(timeoutThread);

                            if (acceptResponse) {
                                error(this.parseRequestText(req));
                            }
                        }));
                    } catch (e) {
                        error(e);
                    }
                });

                var doExecute = mxUtils.bind(this, function (failOnAuth) {
                    try {
                        var acceptResponse = true;
                        var timeoutThread = null;

                        try {
                            timeoutThread = window.setTimeout(mxUtils.bind(this, function () {
                                acceptResponse = false;
                                error({code: App.ERROR_TIMEOUT});
                            }), this.ui.timeout);
                        } catch (e) {
                            // Ignore window closed
                        }

                        var req = new mxXmlRequest(url + '/createUploadSession', '{}', 'POST');

                        req.setRequestHeaders = mxUtils.bind(this, function (request, params) {
                            request.setRequestHeader('Content-Type', 'application/json');
                            request.setRequestHeader('Authorization', 'Bearer ' + _token);

                            if (etag != null) {
                                request.setRequestHeader('If-Match', etag);
                            }
                        });

                        req.send(mxUtils.bind(this, function (req) {
                            window.clearTimeout(timeoutThread);

                            if (acceptResponse) {
                                if (req.getStatus() >= 200 && req.getStatus() <= 299) {
                                    var resp = JSON.parse(req.getText());
                                    uploadPart(resp.uploadUrl, 0);
                                } else if (!failOnAuth && req.getStatus() === 401) {
                                    this.authenticate(function () {
                                        doExecute(true);
                                    }, error, failOnAuth);
                                } else {
                                    error(this.parseRequestText(req), req);
                                }
                            }
                        }), mxUtils.bind(this, function (req) {
                            window.clearTimeout(timeoutThread);

                            if (acceptResponse) {
                                error(this.parseRequestText(req));
                            }
                        }));
                    } catch (e) {
                        error(e);
                    }
                });

                if (_token == null || this.tokenExpiresOn - Date.now() < 60000) //60 sec tolerance window
                {
                    this.authenticate(function () {
                        doExecute(true);
                    }, error);
                } else {
                    doExecute(false);
                }
            } else {
                error({message: mxResources.get('unknownError')});
            }
        } catch (e) {
            error(e);
        }
    };

    /**
     * Translates this point by the given vector.
     *
     * @param {number} dx X-coordinate of the translation.
     * @param {number} dy Y-coordinate of the translation.
     */
    OneDriveClient.prototype.writeFile = function (url, data, method, contentType, success, error, etag) {
        try {
            if (url != null && data != null) {
                var doExecute = mxUtils.bind(this, function (failOnAuth) {
                    try {
                        var acceptResponse = true;
                        var timeoutThread = null;

                        try {
                            timeoutThread = window.setTimeout(mxUtils.bind(this, function () {
                                acceptResponse = false;
                                error({code: App.ERROR_TIMEOUT});
                            }), this.ui.timeout);
                        } catch (e) {
                            // Ignore window closed
                        }

                        var req = new mxXmlRequest(url, data, method);

                        req.setRequestHeaders = mxUtils.bind(this, function (request, params) {
                            // Space deletes content type header. Specification says "text/plain"
                            // should work but returns an 415 Unsupported Media Type error
                            request.setRequestHeader('Content-Type', contentType || ' ');
                            //TODO This header is needed for moving a file between two different drives.
                            //		Note: the response is empty when this header is used, also the server may take some time to really execute the request (i.e. async)
                            //request.setRequestHeader('Prefer', 'respond-async');
                            request.setRequestHeader('Authorization', 'Bearer ' + _token);

                            if (etag != null) {
                                request.setRequestHeader('If-Match', etag);
                            }
                        });

                        req.send(mxUtils.bind(this, function (req) {
                            window.clearTimeout(timeoutThread);

                            if (acceptResponse) {
                                if (req.getStatus() >= 200 && req.getStatus() <= 299) {
                                    if (this.user == null) {
                                        this.updateUser(this.emptyFn, this.emptyFn, true);
                                    }

                                    success(JSON.parse(req.getText()));
                                } else if (!failOnAuth && req.getStatus() === 401) {
                                    this.authenticate(function () {
                                        doExecute(true);
                                    }, error, failOnAuth);
                                } else {
                                    error(this.parseRequestText(req), req);
                                }
                            }
                        }), mxUtils.bind(this, function (req) {
                            window.clearTimeout(timeoutThread);

                            if (acceptResponse) {
                                error(this.parseRequestText(req));
                            }
                        }));
                    } catch (e) {
                        error(e);
                    }
                });

                if (_token == null || this.tokenExpiresOn - Date.now() < 60000) //60 sec tolerance window
                {
                    this.authenticate(function () {
                        doExecute(true);
                    }, error);
                } else {
                    doExecute(false);
                }
            } else {
                error({message: mxResources.get('unknownError')});
            }
        } catch (e) {
            error(e);
        }
    };

    /**
     * Checks if the client is authorized and calls the next step.
     */
    OneDriveClient.prototype.parseRequestText = function (req) {
        var result = {message: mxResources.get('unknownError')};

        try {
            result = JSON.parse(req.getText());
        } catch (e) {
            // ignore
        }

        return result;
    };

    /**
     * Checks if the client is authorized and calls the next step.
     */
    OneDriveClient.prototype.pickLibrary = function (fn) {
        this.pickFile(function (id) {
            // Ignores second argument
            fn(id);
        });
    };

    OneDriveClient.prototype.createInlinePicker = function (fn, foldersOnly) {
        return mxUtils.bind(this, function () {
            var odPicker = null;
            var div = document.createElement('div');
            div.style.width = '550px';
            div.style.height = '435px';
            div.style.position = 'relative';

            var dlg = new CustomDialog(this.ui, div, mxUtils.bind(this, function () {
                var item = odPicker.getSelectedItem();

                if (item != null) {
                    if (foldersOnly && typeof item.folder == 'object') {
                        fn({
                            value: [item]
                        });
                        return;
                    } else if (!item.folder) {
                        fn(OneDriveFile.prototype.getIdOf(item));
                        return;
                    }
                }

                return mxResources.get('invalidSel', null, 'Invalid selection');
            }), null, mxResources.get(foldersOnly ? 'save' : 'open'), null, null, null, null, true);

            this.ui.showDialog(dlg.container, 550, 485, true, true);

            odPicker = new mxODPicker(div, null, mxUtils.bind(this, function (url, success, error) {
                    this.executeRequest(this.baseUrl + url, function (req) {
                        success(JSON.parse(req.getText()));
                    }, error);
                }), mxUtils.bind(this, function (id, driveId, success, error) {
                    this.executeRequest(this.baseUrl + '/drives/' + driveId + '/items/' + id, function (req) {
                        success(JSON.parse(req.getText()));
                    }, error);
                }), null, null, function (item) {
                    if (foldersOnly) //Currently this is not called when in foldersOnly mode
                    {
                        fn({
                            value: [item]
                        });
                    } else {
                        fn(OneDriveFile.prototype.getIdOf(item));
                    }
                },
                mxUtils.bind(this, function (err) {
                    this.ui.showError(mxResources.get('error'), err);
                }), foldersOnly);
        });
    };

    /**
     * Checks if the client is authorized and calls the next step.
     */
    OneDriveClient.prototype.pickFolder = function (fn, direct) {
        var errorFn = mxUtils.bind(this, function (e) {
            this.ui.showError(mxResources.get('error'), e && e.message ? e.message : e);
        });

        var odSaveDlg = mxUtils.bind(this, function (direct) {
            var openSaveDlg = this.inlinePicker ? this.createInlinePicker(fn, true) :
                mxUtils.bind(this, function () {
                    OneDrive.save(
                        {
                            clientId: this.clientId,
                            action: 'query',
                            openInNewWindow: true,
                            advanced:
                                {
                                    'endpointHint': mxClient.IS_IE11 ? null : this.endpointHint, //IE11 doen't work with our modified version, so, setting endpointHint disable using our token BUT will force relogin!
                                    'redirectUri': this.pickerRedirectUri,
                                    'queryParameters': 'select=id,name,parentReference',
                                    'accessToken': _token,
                                    isConsumerAccount: false
                                },
                            success: mxUtils.bind(this, function (files) {
                                fn(files);

                                //Update the token in case a login with a different user
                                if (mxClient.IS_IE11) {
                                    _token = files.accessToken;
                                }
                            }),
                            cancel: mxUtils.bind(this, function () {
                                // do nothing
                            }),
                            error: errorFn
                        });
                });

            if (direct) {
                openSaveDlg();
            } else {
                this.ui.confirm(mxResources.get('useRootFolder'), mxUtils.bind(this, function () {
                    fn({value: [{id: 'root', name: 'root', parentReference: {driveId: 'me'}}]});

                }), openSaveDlg, mxResources.get('yes'), mxResources.get('noPickFolder') + '...', true);
            }

            if (this.user == null) {
                this.updateUser(this.emptyFn, this.emptyFn, true);
            }
        });

        if (_token == null || this.tokenExpiresOn - Date.now() < 60000) //60 sec tolerance window
        {
            this.authenticate(mxUtils.bind(this, function () {
                odSaveDlg(false);
            }), errorFn);
        } else {
            odSaveDlg(direct);
        }
    };

    /**
     * Checks if the client is authorized and calls the next step.
     */
    OneDriveClient.prototype.pickFile = function (fn) {
        fn = (fn != null) ? fn : mxUtils.bind(this, function (id) {
            this.ui.loadFile('W' + encodeURIComponent(id));
        });

        var errorFn = mxUtils.bind(this, function (e) {
            this.ui.showError(mxResources.get('error'), e && e.message ? e.message : e);
        });

        var odOpenDlg = this.inlinePicker ? this.createInlinePicker(fn) :
            mxUtils.bind(this, function () {
                OneDrive.open(
                    {
                        clientId: this.clientId,
                        action: 'query',
                        multiSelect: false,
                        advanced:
                            {
                                'endpointHint': mxClient.IS_IE11 ? null : this.endpointHint, //IE11 doen't work with our modified version, so, setting endpointHint disable using our token BUT will force relogin!
                                'redirectUri': this.pickerRedirectUri,
                                'queryParameters': 'select=id,name,parentReference', //We can also get @microsoft.graph.downloadUrl within this request but it will break the normal process
                                'accessToken': _token,
                                isConsumerAccount: false
                            },
                        success: mxUtils.bind(this, function (files) {
                            if (files != null && files.value != null && files.value.length > 0) {
                                //Update the token in case a login with a different user
                                if (mxClient.IS_IE11) {
                                    _token = files.accessToken;
                                }

                                fn(OneDriveFile.prototype.getIdOf(files.value[0]), files);
                            }
                        }),
                        cancel: mxUtils.bind(this, function () {
                            // do nothing
                        }),
                        error: errorFn
                    });

                if (this.user == null) {
                    this.updateUser(this.emptyFn, this.emptyFn, true);
                }
            });

        if (_token == null || this.tokenExpiresOn - Date.now() < 60000) //60 sec tolerance window
        {
            this.authenticate(mxUtils.bind(this, function () {
                this.ui.showDialog(new BtnDialog(this.ui, this, mxResources.get('open'), mxUtils.bind(this, function () {
                    this.ui.hideDialog();
                    odOpenDlg();
                })).container, 300, 140, true, true);
            }), errorFn);
        } else {
            odOpenDlg();
        }
    };

    /**
     * Checks if the client is authorized and calls the next step.
     */
    OneDriveClient.prototype.logout = function () {
        if (isLocalStorage) {
            var check = localStorage.getItem('odpickerv7cache');

            if (check != null && check.substring(0, 19) == '{"odsdkLoginHint":{') {
                localStorage.removeItem('odpickerv7cache');
            }
        }

        window.open('https://login.microsoftonline.com/common/oauth2/v2.0/logout', 'logout', 'width=525,height=525,status=no,resizable=yes,toolbar=no,menubar=no,scrollbars=yes');
        //Send to server to clear refresh token cookie
        this.ui.editor.loadUrl(this.redirectUri + '?doLogout=1&state=' + encodeURIComponent('cId=' + this.clientId + '&domain=' + window.location.hostname));
        this.clearPersistentToken();
        this.setUser(null);
        _token = null;
    };

})();